// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:code_builder/code_builder.dart';
import 'package:dart_style/dart_style.dart';
import 'package:path/path.dart' as path;
import 'package:pub_semver/pub_semver.dart';
import 'package:source_gen/source_gen.dart';
import 'package:worker_bee/worker_bee.dart';
import 'package:worker_bee_builder/src/impl/common.dart';
import 'package:worker_bee_builder/src/impl/js.dart';
import 'package:worker_bee_builder/src/impl/vm.dart';

/// Creates an emitter instance with common configuration.
DartEmitter createEmitter() => DartEmitter(
  allocator: Allocator(),
  orderDirectives: true,
  useNullSafetySyntax: true,
);

/// The common formatter for generated code.
final formatter = DartFormatter(languageVersion: Version(3, 9, 0));

/// Common header for generated outputs.
const generatedHeader = '// Generated by worker_bee_builder.';

/// {@macro worker_bee_builder.worker_bee_builder}
class WorkerBeeGenerator extends GeneratorForAnnotation<WorkerBee> {
  @override
  Future<String> generateForAnnotatedElement(
    Element element,
    ConstantReader annotation,
    BuildStep buildStep,
  ) async {
    if (element is! ClassElement) {
      throw ArgumentError('@WorkerBee can only be applied to classes.');
    }

    // Get generic arguments
    final supertype = element.supertype;
    if (supertype == null || (supertype.element.name != 'WorkerBeeBase')) {
      throw ArgumentError(
        '@WorkerBee classes must extends WorkerBeeBase<M, R>.',
      );
    }
    final typeArgs = supertype.typeArguments;
    if (typeArgs.length < 2) {
      throw ArgumentError('@WorkerBee classes must declare their types.');
    }

    // Look up message type to generate JS/VM implementations.
    final requestType = typeArgs[0];
    final requestTypeEl = requestType.element;
    if (requestTypeEl == null || requestTypeEl is! ClassElement) {
      final requestTypeName =
          // TODO(Jordan-Nelson): remove use of `withNullability` when min dart version is 3.4 or higher
          // ignore: deprecated_member_use
          requestType.getDisplayString(withNullability: true);
      throw ArgumentError('Could not find element for $requestTypeName.');
    }

    final responseType = typeArgs[1];
    final responseTypeEl = responseType.element;

    final declaresJsEntrypoint = element.fields.any(
      (el) => el.name == 'jsEntrypoint' && !el.isAbstract,
    );
    final declaresFallbackUrls = element.fields.any(
      (el) => el.name == 'fallbackUrls' && !el.isAbstract,
    );

    final packageName = buildStep.inputId.package;
    final hiveId = AssetId(
      packageName,
      annotation.read('hivePath').stringValue,
    );

    final workerImpls = _generateWorkerImpls(
      element,
      requestTypeEl,
      responseTypeEl as ClassElement?,
      declaresJsEntrypoint,
      declaresFallbackUrls,
      hiveId,
    );

    final libraries = <Target, String>{};
    for (final workerImpl in workerImpls) {
      final target = workerImpl.target.name;
      final assetId = buildStep.inputId.changeExtension('.worker.$target.dart');
      libraries[workerImpl.target] = path.basename(assetId.path);
      await buildStep.writeAsString(assetId, '''
$generatedHeader

${workerImpl.impl}''');
    }

    return '''
$generatedHeader

export '${libraries[Target.vm]}' 
  if (dart.library.js_interop) '${libraries[Target.js]}';
''';
  }

  List<WorkerImpl> _generateWorkerImpls(
    ClassElement workerEl,
    ClassElement messageTypeEl,
    ClassElement? resultTypeEl,
    bool declaresJsEntrypoint,
    bool declaresFallbackUrls,
    AssetId hiveEntrypointId,
  ) {
    final vmClass = VmGenerator(
      workerEl,
      messageTypeEl,
      resultTypeEl,
    ).generate();
    final jsClass = JsGenerator(
      workerEl,
      messageTypeEl,
      resultTypeEl,
      declaresJsEntrypoint: declaresJsEntrypoint,
      declaresFallbackUrls: declaresFallbackUrls,
      hiveEntrypointId: hiveEntrypointId,
    ).generate();

    return [
      WorkerImpl(
        Target.vm,
        formatter.format('${vmClass.accept(createEmitter())}'),
      ),
      WorkerImpl(
        Target.js,
        formatter.format('${jsClass.accept(createEmitter())}'),
      ),
    ];
  }
}
